package handler

import (
	"Projects/NoteSync/internal/common"
	"Projects/NoteSync/internal/config"
	"Projects/NoteSync/internal/util"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/go-basic/uuid"
	"github.com/qf-tech/qftech-base/pkg/log"
	qflog "github.com/qf-tech/qftech-base/pkg/log"
)

type Handler struct {
}

// JSONResult def
type JSONResult struct {
	Status     int    `json:"status"`
	Data       string `json:"data"`
	ClientAddr string `json:"clientAddr,omitempty"`
}

func (h Handler) genTraceID() string {
	uid := uuid.New()
	tmpSlice := strings.Split(uid, "-")
	slicelen := len(tmpSlice)
	traceID := tmpSlice[slicelen-1]
	return traceID
}

func (h Handler) uploadLogFile(filePath string, dstDirPath string) (string, error) {
	server := config.SvrConfig.Server
	wsse := util.GenWsse(server.Username, server.Password, h.genTraceID())
	url := fmt.Sprintf("http://%s:%d/cava3/upload", server.HttpIP, server.HttpPort)
	if server.IsSSL {
		url = fmt.Sprintf("https://%s:%d/cava3/upload", server.HttpIP, server.HttpPort)
	}

	headers := map[string]string{
		"X-WSSE":          wsse,
		"UPLOAD-DIR-PATH": dstDirPath,
		"FORCE-UPLOAD":    "TRUE",
	}

	dataResp, err := util.HttpPostFile(filePath, url, headers, 20*time.Second)
	if err != nil {
		qflog.Sugare.Errorf("post xls file failed err: %v", err)
		return "", err
	}

	retResp, err := json.Marshal(dataResp)
	if err != nil {
		qflog.Sugare.Errorf("marshal resp err: %v", err)
		return "", err
	}

	var resp JSONResult
	err = json.Unmarshal(retResp, &resp)
	if err != nil {
		qflog.Sugare.Errorf("unmarshal resp err: %v", err)
		return "", err
	}

	if !strings.Contains(resp.Data, "upload succeed") {
		return "", errors.New("upload fail")
	}

	pos := strings.Index(resp.Data, "path:")
	path := resp.Data[pos+len("path:"):]
	return path, nil
}

type FileInfo struct {
	AbsolutePath string
	BaseName     string
	Type         string
	Md5          string
	UpdatedAt    time.Time
	UploadedAt   time.Time
	Ctx          context.Context
	Cancel       context.CancelFunc
}

var fileSyncing map[string]bool
var fileInfoMutex sync.RWMutex
var fileInfo map[string]*FileInfo

func getFileInfo(ctx context.Context, fileDir string, format string) {
	bodyInfos, err := ioutil.ReadDir(fileDir)
	if err != nil {
		log.Sugare.Errorf("read dir err: %v", err)
		return
	}

	fileInfoMutex.Lock()
	defer fileInfoMutex.Unlock()
	{
		for _, f := range bodyInfos {
			name := f.Name()
			path := fmt.Sprintf("%s/%s", fileDir, name)
			if format != "*" && !strings.HasSuffix(name, format) {
				// log.Sugare.Warnf("won't add to file info, filePath: %s, expect format: %s", path, format)
				continue
			}

			if _, ok := fileInfo[path]; ok {
				fileInfo[path].UpdatedAt = time.Now()
				log.Sugare.Infof("updated, filePath: %s", fileInfo[path].AbsolutePath)
				continue
			}

			ctx, cancel := context.WithCancel(context.Background())
			md5, _ := util.GetFileMd5(path)
			fileInfo[path] = &FileInfo{
				AbsolutePath: path,
				BaseName:     name,
				Type:         "unknown",
				UpdatedAt:    time.Now(),
				UploadedAt:   time.Now(),
				Ctx:          ctx,
				Cancel:       cancel,
				Md5:          md5,
			}
		}

		for _, e := range fileInfo {
			now := time.Now()
			if now.Sub(e.UpdatedAt) >= 1*time.Minute {
				e.Cancel()
				log.Sugare.Warnf("now goroutine will exit, filePath: %s", e.AbsolutePath)
			}
		}
	}

}

func (h Handler) StartWatch(ctx context.Context, filePath string, relaDirPath string) {
	// 监控文件
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		qflog.Sugare.Errorf("error: %v", err)
	}
	// defer watcher.Close()

	go func() {
		exitFlag := false
		for {
			if exitFlag {
				qflog.Sugare.Warnf("goroutine exit, filePath: %s", filePath)
				fileInfoMutex.Lock()
				delete(fileInfo, filePath)
				fileInfoMutex.Unlock()
				break
			}

			select {
			case <-ctx.Done():
				exitFlag = true
			case event, ok := <-watcher.Events:
				if !ok {
					return
				}
				qflog.Sugare.Infof("event: %v", event)
				if event.Op&fsnotify.Write == fsnotify.Write {
					now := time.Now()
					uploadFile := filePath
					// qflog.Sugare.Infof("modified event, file: %s", event.Name)
					d := time.Duration(5 * time.Second)
					if now.Sub(fileInfo[filePath].UploadedAt) < d {
						nextUploadedAt := fileInfo[filePath].UploadedAt.Add(d).Format(time.RFC3339)
						qflog.Sugare.Warnf("won't sync until: %v, filePath: %s, UploadedAt: %v", nextUploadedAt, filePath, fileInfo[filePath].UploadedAt)
						continue
					}

					if strings.HasSuffix(filePath, ".md") && config.SvrConfig.Server.Trans2Html {
						htmlPath := strings.ReplaceAll(filePath, ".md", ".html")
						if err := util.Trans2HtmlByTmpl(filePath, htmlPath, config.SvrConfig.Server.Theme); err != nil {
							qflog.Sugare.Errorf("markdown trans to html err: %v", err)
							continue
						}
						qflog.Sugare.Warnf("markdown trans to html ok, htmlPath: %s", htmlPath)
						uploadFile = htmlPath
					}

					url, err := h.uploadLogFile(uploadFile, relaDirPath)
					if err != nil {
						qflog.Sugare.Errorf("upload file failed err: %v", err)
						continue
					}
					fileInfoMutex.Lock()
					fileInfo[filePath].UploadedAt = now
					fileInfoMutex.Unlock()
					qflog.Sugare.Infof("upload ok, url: %s", url)
				}
			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				qflog.Sugare.Errorf("error: %v", err)
			}
		}
	}()

	err = watcher.Add(filePath)
	if err != nil {
		qflog.Sugare.Errorf("error: %v", err)
	}
	qflog.Sugare.Infof("start watch, file: %s", filePath)
}

var pathSeparator string

func (h Handler) StartWatchWithMD5(ctx context.Context, filePath string, relaDirPath string) {
	pathSeparator = "/"
	if strings.ToLower(runtime.GOOS) == "windows" {
		pathSeparator = "\\"
	}
	go func() {
		exitFlag := false
		for {
			if exitFlag {
				qflog.Sugare.Warnf("goroutine exit, filePath: %s", filePath)
				fileInfoMutex.Lock()
				delete(fileInfo, filePath)
				fileInfoMutex.Unlock()
				break
			}

			// 超时机制
			timeout := make(chan bool, 1)
			go func() {
				time.Sleep(10 * time.Second)
				timeout <- true
			}()

			select {
			case <-ctx.Done():
				exitFlag = true
			case <-timeout:
				now := time.Now()
				// qflog.Sugare.Infof("modified event, file: %s", event.Name)
				uploadFile := filePath
				calMD5, _ := util.GetFileMd5(filePath)
				if calMD5 == fileInfo[filePath].Md5 {
					// qflog.Sugare.Warnf("won't sync, not change, filePath: %s, md5: %s", filePath, calMD5)
					continue
				}
				d := time.Duration(5 * time.Second)
				if now.Sub(fileInfo[filePath].UploadedAt) < d {
					nextUploadedAt := fileInfo[filePath].UploadedAt.Add(d).Format(time.RFC3339)
					qflog.Sugare.Warnf("won't sync until: %v, filePath: %s, UploadedAt: %v", nextUploadedAt, filePath, fileInfo[filePath].UploadedAt)
					continue
				}

				if strings.HasSuffix(filePath, ".md") && config.SvrConfig.Server.Trans2Html {
					basename := util.BaseName(filePath, "")
					dirPath := filepath.Dir(filePath)
					htmlDirPath := fmt.Sprintf("%s%shtml", dirPath, pathSeparator)
					_ = util.MakeDirExist(htmlDirPath)
					htmlPath := fmt.Sprintf("%s%s%s", htmlDirPath, pathSeparator, strings.ReplaceAll(basename, ".md", ".html"))
					if err := util.Trans2HtmlByTmpl(filePath, htmlPath, config.SvrConfig.Server.Theme); err != nil {
						qflog.Sugare.Errorf("markdown trans to html err: %v", err)
						continue
					}
					qflog.Sugare.Warnf("markdown trans to html ok, htmlPath: %s", htmlPath)

					// 原始 markdown 文件上传到 ./md
					_, err := h.uploadLogFile(uploadFile, relaDirPath+"/md")
					if err != nil {
						qflog.Sugare.Errorf("upload md file failed err: %v", err)
						continue
					}
					uploadFile = htmlPath
				}

				url, err := h.uploadLogFile(uploadFile, relaDirPath)
				if err != nil {
					qflog.Sugare.Errorf("upload file failed err: %v", err)
					continue
				}

				fileInfoMutex.Lock()
				fileInfo[filePath].UploadedAt = now
				fileInfo[filePath].Md5 = calMD5
				fileInfoMutex.Unlock()
				qflog.Sugare.Infof("upload ok, url: %s", url)
			}
		}
	}()

	qflog.Sugare.Infof("start watch, file: %s", filePath)
}

// 循环获取新的文件，并启动同步更新的协程
func FileSyncLoop(fileDir string, relaDirPath string) {
	fileSyncing = make(map[string]bool)
	fileInfo = make(map[string]*FileInfo)
	handle := Handler{}

	for {
		ctx := util.GenTraceCtx()
		log := log.GetLogger().GetCtx(ctx).Sugar()
		getFileInfo(ctx, fileDir, config.SvrConfig.Server.FileFormat)

		fileInfoMutex.Lock()
		for k, v := range fileInfo {
			if syncing, ok := fileSyncing[k]; ok && syncing {
				continue
			}

			if config.SvrConfig.Server.SyncMode == common.SyncModeNull ||
				config.SvrConfig.Server.SyncMode == common.SyncModeMd5 {
				go handle.StartWatchWithMD5(v.Ctx, v.AbsolutePath, relaDirPath)
			} else if config.SvrConfig.Server.SyncMode == common.SyncModeWatch {
				go handle.StartWatch(v.Ctx, v.AbsolutePath, relaDirPath)
			}
			fileSyncing[v.AbsolutePath] = true
			log.Infof("start sync file goroutine ok, filePath: %s", v.AbsolutePath)
		}
		fileInfoMutex.Unlock()
		time.Sleep(time.Duration(common.DirFileScanInternal) * time.Second)
	}
}
